/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package hivemall.model;

import hivemall.model.WeightValueWithClock.WeightValueParamsF1Clock;
import hivemall.model.WeightValueWithClock.WeightValueParamsF2Clock;
import hivemall.model.WeightValueWithClock.WeightValueParamsF3Clock;
import hivemall.model.WeightValueWithClock.WeightValueWithCovarClock;
import hivemall.utils.collections.IMapIterator;
import hivemall.utils.collections.maps.OpenHashTable;

import javax.annotation.Nonnull;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public final class SparseModel extends AbstractPredictionModel {
    private static final Log logger = LogFactory.getLog(SparseModel.class);

    @Nonnull
    private final OpenHashTable<Object, IWeightValue> weights;
    private final boolean hasCovar;
    private boolean clockEnabled;

    public SparseModel(int size, boolean hasCovar) {
        super();
        this.weights = new OpenHashTable<Object, IWeightValue>(size);
        this.hasCovar = hasCovar;
        this.clockEnabled = false;
    }

    @Override
    protected boolean isDenseModel() {
        return false;
    }

    @Override
    public boolean hasCovariance() {
        return hasCovar;
    }

    @Override
    public void configureParams(boolean sum_of_squared_gradients, boolean sum_of_squared_delta_x,
            boolean sum_of_gradients) {}

    @Override
    public void configureClock() {
        this.clockEnabled = true;
    }

    @Override
    public boolean hasClock() {
        return clockEnabled;
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T extends IWeightValue> T get(@Nonnull final Object feature) {
        return (T) weights.get(feature);
    }

    @Override
    public <T extends IWeightValue> void set(@Nonnull final Object feature,
            @Nonnull final T value) {
        final IWeightValue wrapperValue = wrapIfRequired(value);

        if (clockEnabled && value.isTouched()) {
            IWeightValue old = weights.get(feature);
            if (old != null) {
                short newclock = (short) (old.getClock() + (short) 1);
                wrapperValue.setClock(newclock);
                int newDelta = old.getDeltaUpdates() + 1;
                wrapperValue.setDeltaUpdates((byte) newDelta);
            }
        }
        weights.put(feature, wrapperValue);

        onUpdate(feature, wrapperValue);
    }

    @Override
    public void delete(@Nonnull Object feature) {
        weights.remove(feature);
    }

    @Nonnull
    private IWeightValue wrapIfRequired(@Nonnull final IWeightValue value) {
        final IWeightValue wrapper;
        if (clockEnabled) {
            switch (value.getType()) {
                case NoParams:
                    wrapper = new WeightValueWithClock(value);
                    break;
                case ParamsCovar:
                    wrapper = new WeightValueWithCovarClock(value);
                    break;
                case ParamsF1:
                    wrapper = new WeightValueParamsF1Clock(value);
                    break;
                case ParamsF2:
                    wrapper = new WeightValueParamsF2Clock(value);
                    break;
                case ParamsF3:
                    wrapper = new WeightValueParamsF3Clock(value);
                    break;
                default:
                    throw new IllegalStateException("Unexpected value type: " + value.getType());
            }
        } else {
            wrapper = value;
        }
        return wrapper;
    }

    @Override
    public float getWeight(@Nonnull final Object feature) {
        IWeightValue v = weights.get(feature);
        return v == null ? 0.f : v.get();
    }

    @Override
    public void setWeight(@Nonnull Object feature, float value) {
        throw new UnsupportedOperationException();
    }

    @Override
    public float getCovariance(@Nonnull final Object feature) {
        IWeightValue v = weights.get(feature);
        return v == null ? 1.f : v.getCovariance();
    }

    @Override
    protected void _set(@Nonnull final Object feature, final float weight, final short clock) {
        final IWeightValue w = weights.get(feature);
        if (w == null) {
            logger.warn("Previous weight not found: " + feature);
            throw new IllegalStateException("Previous weight not found " + feature);
        }
        w.set(weight);
        w.setClock(clock);
        w.setDeltaUpdates(BYTE0);
    }

    @Override
    protected void _set(@Nonnull final Object feature, final float weight, final float covar,
            final short clock) {
        final IWeightValue w = weights.get(feature);
        if (w == null) {
            logger.warn("Previous weight not found: " + feature);
            throw new IllegalStateException("Previous weight not found: " + feature);
        }
        w.set(weight);
        w.setCovariance(covar);
        w.setClock(clock);
        w.setDeltaUpdates(BYTE0);
    }

    @Override
    public int size() {
        return weights.size();
    }

    @Override
    public boolean contains(@Nonnull final Object feature) {
        return weights.containsKey(feature);
    }

    @SuppressWarnings("unchecked")
    @Override
    public <K, V extends IWeightValue> IMapIterator<K, V> entries() {
        return (IMapIterator<K, V>) weights.entries(true);
    }

}
